package com.clementheliou.cheliou.web.controller.user;

import static org.apache.commons.lang3.StringUtils.isEmpty;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Controller;

import com.clementheliou.cheliou.business.technical.security.authentication.AuthenticationDetailsTechnicalService;
import com.clementheliou.cheliou.business.technical.security.authentication.model.AuthenticationDetails;
import com.clementheliou.cheliou.dal.model.member.enumeration.MemberLocale;
import com.clementheliou.cheliou.dal.model.member.enumeration.MemberRole;
import com.clementheliou.cheliou.util.codec.binary.Base64Utils;
import com.clementheliou.cheliou.web.util.cookie.CookieUtils;
import com.clementheliou.cheliou.web.util.jsf.JSFUtils;

/**
 * Controller dedicated to the user details. Because it uses the Spring Security
 * mecanism, we can share this bean between sessions. However, a cache system
 * exists to reduce the calls between the business and the web layers.
 * 
 * @author Clément HELIOU (clement.heliou@gmail.com)
 * @see {@link Controller};
 * @see {@link Serializable}.
 * @since 1.0
 */
@Controller
public class UserBean implements Serializable {

	/**
	 * Logger for this class.
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @see {@link Logger};
	 * @see {@link LoggerFactory}.
	 * @since 1.0
	 */
	private static final Logger LOGGER = LoggerFactory.getLogger(UserBean.class);

	/**
	 * UID for serialization.
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @since 1.0
	 */
	private static final long serialVersionUID = 2841948272585764352L;

	/**
	 * Utilities about {@link Cookie}'s.
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @see {@link Autowired};
	 * @see {@link Cookie};
	 * @see {@link CookieUtils}.
	 * @since 1.0
	 */
	@Autowired
	private transient CookieUtils cookieUtils;

	/**
	 * Service used to access user details.
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @see {@link Autowired};
	 * @see {@link AuthenticationDetailsTechnicalService}.
	 * @since 1.0
	 */
	@Autowired
	private transient AuthenticationDetailsTechnicalService authenticationDetailsService;

	/**
	 * Utilities about JSF.
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @see {@link Autowired};
	 * @see {@link JSFUtils}.
	 * @since 1.0
	 */
	@Autowired
	private transient JSFUtils jsfUtils;

	/**
	 * Application cache used for (anonymous or not) users . By this way, we
	 * avoid unnecessary calls between business and web layers.
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @see {@link AuthenticationDetails}.
	 * @since 1.0
	 */
	private Map<String, AuthenticationDetails> membersCache = new HashMap<String, AuthenticationDetails>();

	/**
	 * Application cache used for user's locales. By this way, we avoid
	 * unnecessary calls between business and web layers.
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @see {@link Locale}.
	 * @since 1.0
	 */
	private Map<String, Locale> localesCache = new HashMap<String, Locale>();

	/**
	 * <p>
	 * Gets the current user; i.e. the one related to the current
	 * {@link HttpSession}. It may be an anonymous or a connected user. At the
	 * first time, the value is retrieved from the Spring Security context and
	 * is put in the cache. The following times, the returned value is the one
	 * of this cache.
	 * </p>
	 * <p>
	 * In some uncommon cases, the session may be null; for example at the very
	 * beginning of the home page loading. In such a case, we return the value
	 * from the Spring Security context.
	 * </p>
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @return the user related to the current {@link HttpSession}.
	 * @see {@link AuthenticationDetails};
	 * @see {@link HttpSession}.
	 * @since 1.0
	 */
	public AuthenticationDetails getCurrentUser() {
		final HttpSession session = jsfUtils.getCurrentSession();
		final AuthenticationDetails cachedUser = (session != null) ? membersCache.get(session.getId()) : null;

		if (session != null) {
			if (null == cachedUser) {
				final AuthenticationDetails userDetails = authenticationDetailsService.getCurrentUser();
				LOGGER.debug("Put the user details in cache for the session id '{}'.", session.getId());
				membersCache.put(session.getId(), userDetails);
			}

			LOGGER.debug("Get the user cache value for the session id '{}'.", session.getId());
			return membersCache.get(session.getId());
		}

		LOGGER.debug("No session; user will be retrieved from Spring Security context.");
		return authenticationDetailsService.getCurrentUser();
	}

	/**
	 * <p>
	 * Gets the locale to be used at this time. Checks if this value can be find
	 * in the cache and returns it if this action is a success. Otherwise, it
	 * finds the current locale to be used through {@link #findCurrentLocale()}
	 * and put this value into the cache.
	 * </p>
	 * <p>
	 * In some uncommon cases, the session may be null; for example at the very
	 * beginning of the home page loading. In such a case, we have to find the
	 * locale to be used through the {@link #findCurrentLocale()}.
	 * </p>
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @return the locale to be used at the present time.
	 * @see {@link Locale}.
	 * @since 1.0
	 */
	public Locale getCurrentLocale() {
		final HttpSession session = jsfUtils.getCurrentSession();

		if (session != null) {
			if (null == localesCache.get(session.getId())) {
				LOGGER.debug("Put the user locale in cache for the session id '{}'.", session.getId());
				localesCache.put(session.getId(), findCurrentLocale());
			}

			LOGGER.debug("Get the user locale value for the session id '{}'.", session.getId());
			return localesCache.get(session.getId());
		}

		LOGGER.debug("No session; locale will be retrieved from other sources. See further for details.");
		return findCurrentLocale();
	}

	/**
	 * <p>
	 * Gets the locale to be used at this time. If there is a connected user,
	 * its locale will be returned. Else, if only one locale cookie exists on
	 * this computer, its value will be used. Otherwise, the browser locale will
	 * be returned. Finally, if this last one is not supported by the
	 * application, {@link Locale#FRANCE} will be chosen.
	 * </p>
	 * <p>
	 * This method should only be used when there is <b>no cached value</b> for
	 * the current locale.
	 * </p>
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @return the locale to be used at the present time.
	 * @see {@link Locale}.
	 * @since 1.0
	 */
	private Locale findCurrentLocale() {
		final AuthenticationDetails userDetails = getCurrentUser();

		if (isUser(userDetails)) {
			LOGGER.debug("User locale found!");
			return userDetails.getMemberLocale().getLocale();
		}

		final HttpServletRequest request = jsfUtils.getExternalContextRequest();
		final Cookie localeCookie = cookieUtils.getLocaleCookie(request);

		if (null != localeCookie) {
			LOGGER.debug("Locale cookie found!");
			final String[] cookieValue = Base64Utils.decodeBase64(localeCookie.getValue()).split(CookieUtils.LOCALE_COOKIE_VALUE_SEPARATOR);
			return new Locale(cookieValue[0], cookieValue[1]);
		}

		final Locale browserLocale = request.getLocale();
		final boolean isBrowerLocaleAccepted = MemberLocale.isLocaleAccepted(browserLocale);

		LOGGER.debug("Browser locale accepted: {}.", isBrowerLocaleAccepted);
		return (isBrowerLocaleAccepted) ? browserLocale : Locale.FRANCE;
	}

	/**
	 * Is the current visitor connected? Is he an administrator?
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @return <code>true</code> if there is a connected administrator,
	 *         <code>false</code> otherwise.
	 * @since 1.0
	 */
	public boolean isAdministrator() {
		return isAdministrator(getCurrentUser());
	}

	/**
	 * Is the given visitor connected? Is he an administrator?
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @return <code>true</code> if the given visitor is a connected
	 *         administrator, <code>false</code> otherwise.
	 * @since 1.0
	 */
	private boolean isAdministrator(final AuthenticationDetails userDetails) {
		return (!isEmpty(userDetails.getUsername()) && userDetails.getAuthorities().contains(new SimpleGrantedAuthority(MemberRole.ADMINISTRATOR.getCode())));
	}

	/**
	 * Is the current visitor connected? Is he an user?
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @return <code>true</code> if there is a connected user,
	 *         <code>false</code> otherwise.
	 * @since 1.0
	 */
	public boolean isUser() {
		return isUser(getCurrentUser());
	}

	/**
	 * Is the given visitor connected? Is he an user?
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @param userDetails the given visitor to be checked.
	 * @return <code>true</code> if the given user is a connected user,
	 *         <code>false</code> otherwise.
	 * @since 1.0
	 */
	private boolean isUser(final AuthenticationDetails userDetails) {
		return (!isEmpty(userDetails.getUsername()) && userDetails.getAuthorities().contains(new SimpleGrantedAuthority(MemberRole.USER.getCode())));
	}

	/**
	 * Updates the connected user first name. This method should be used with
	 * care because it will update the Spring Security user entity. Please note
	 * that this method is forbidden if there is no connected user.
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @param firstName the first name value to be set.
	 * @since 1.0
	 */
	public void setFirstName(final String firstName) {
		final AuthenticationDetails userDetails = getCurrentUser();

		if (userDetails.getFirstName() == null) {
			throw new UnsupportedOperationException();
		}

		userDetails.setFirstName(firstName);
	}

	/**
	 * Updates the connected user name. This method should be used with care
	 * because it will update the Spring Security user entity. Please note that
	 * this method is forbidden if there is no connected user.
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @param name the name value to be set.
	 * @since 1.0
	 */
	public void setName(final String name) {
		final AuthenticationDetails userDetails = getCurrentUser();

		if (userDetails.getName() == null) {
			throw new UnsupportedOperationException();
		}

		userDetails.setName(name);
	}

	/**
	 * Updates the connected locale. This method should be used with care
	 * because it will update the Spring Security user entity. Please note that
	 * this method is forbidden if there is no connected user.
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @param memberLocale the locale value to be set.
	 * @since 1.0
	 */
	public void setMemberLocale(final MemberLocale memberLocale) {
		final AuthenticationDetails userDetails = getCurrentUser();

		if (userDetails.getMemberLocale() == null) {
			throw new UnsupportedOperationException();
		}

		final HttpSession session = jsfUtils.getCurrentSession();
		LOGGER.debug("Update locale cache with '{}' value for session '{}'.", memberLocale.getLocale(), session.getId());
		localesCache.put(session.getId(), memberLocale.getLocale());
		getCurrentUser().setMemberLocale(memberLocale);
	}

	/**
	 * Method called when a {@link HttpSession} is destroyed. At this time, the
	 * related values are removed from the caches.
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @param se the session destroying event.
	 * @since 1.0
	 */
	public void clearSessionCaches(HttpSessionEvent se) {
		final Object sessionId = se.getSession().getId();

		if (localesCache.containsKey(sessionId)) {
			LOGGER.debug("Clean locale cache for session '{}'.", sessionId);
			localesCache.remove(sessionId);
		}

		if (membersCache.containsKey(sessionId)) {
			LOGGER.debug("Clean user cache for session '{}'.", sessionId);
			membersCache.remove(sessionId);
		}
	}
}
